package indigo.captcha;

import static com.google.common.base.Preconditions.checkArgument;

import java.awt.Color;
import java.awt.Font;
import java.awt.image.BufferedImage;
import java.io.Serializable;
import java.security.SecureRandom;
import java.util.List;
import java.util.Locale;
import java.util.Random;

import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;

import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.octo.captcha.CaptchaException;
import com.octo.captcha.component.image.backgroundgenerator.BackgroundGenerator;
import com.octo.captcha.component.image.backgroundgenerator.UniColorBackgroundGenerator;
import com.octo.captcha.component.image.color.ColorGenerator;
import com.octo.captcha.component.image.color.SingleColorGenerator;
import com.octo.captcha.component.image.deformation.ImageDeformation;
import com.octo.captcha.component.image.fontgenerator.FontGenerator;
import com.octo.captcha.component.image.fontgenerator.RandomFontGenerator;
import com.octo.captcha.component.image.textpaster.DecoratedRandomTextPaster;
import com.octo.captcha.component.image.textpaster.TextPaster;
import com.octo.captcha.component.image.textpaster.textdecorator.BaffleTextDecorator;
import com.octo.captcha.component.image.textpaster.textdecorator.LineTextDecorator;
import com.octo.captcha.component.image.textpaster.textdecorator.TextDecorator;
import com.octo.captcha.component.image.wordtoimage.DeformedComposedWordToImage;
import com.octo.captcha.component.image.wordtoimage.WordToImage;
import com.octo.captcha.component.word.wordgenerator.RandomWordGenerator;
import com.octo.captcha.component.word.wordgenerator.WordGenerator;
import com.octo.captcha.engine.image.DefaultImageCaptchaEngine;
import com.octo.captcha.engine.image.ImageCaptchaEngine;
import com.octo.captcha.image.ImageCaptcha;
import com.octo.captcha.image.ImageCaptchaFactory;

import indigo.captcha.image.OffsetDeformer;
import indigo.captcha.image.RandomDarkenDeformer;
import indigo.captcha.image.RandomInvertDeformer;
import indigo.captcha.image.RandomRippleDeformer;

public class ImageCaptchaEngineFactoryBean implements FactoryBean<ImageCaptchaEngine>, InitializingBean {
	private static final String DEFAULT_CHALLENGE_CHARS = "0123456789";

	private int width;
	private int height;
	private int length;
	private int maxFontSize;
	private Font[] fonts;
	private String[] fontNames;
	private boolean caseSensitive;
	private String challengeChars;

	private ImageCaptchaEngine instance;

	protected TextPaster createTextPaster() {
		ColorGenerator color = new SingleColorGenerator(Color.WHITE);
		TextDecorator[] decorators = new TextDecorator[] {
				new LineTextDecorator(1, Color.WHITE),
				new BaffleTextDecorator(1, Color.WHITE)
		};
		return new DecoratedRandomTextPaster(length, length, color, false, decorators);
	}

	protected BackgroundGenerator createBackgroundGenerator() {
		ColorGenerator color = new SingleColorGenerator(Color.WHITE);
		return new UniColorBackgroundGenerator(width, height, color);
	}

	protected FontGenerator createFontGenerator() {
		int minFontSize = (int) Math.ceil(maxFontSize * 0.8);
		return new RandomFontGenerator(minFontSize, maxFontSize, fonts);
	}

	protected ImageDeformation[] createBackgroundDeformations() {
		return new ImageDeformation[] {
				new RandomDarkenDeformer(0f, 0.3f)
		};
	}

	protected ImageDeformation[] createTextDeformations() {
		return new ImageDeformation[] {
				new OffsetDeformer(height, maxFontSize),
				new RandomDarkenDeformer(0.7f, 1f)
		};
	}

	protected ImageDeformation[] createFinalDeformations() {
		return new ImageDeformation[] {
				new RandomRippleDeformer(width, height, maxFontSize),
				new RandomInvertDeformer()
		};
	}

	protected WordGenerator createWordGenerator() {
		return new RandomWordGenerator(challengeChars);
	}

	private ImageCaptchaEngine createInstance() {
		WordGenerator wordGenerator = createWordGenerator();
		TextPaster textPaster = createTextPaster();
		BackgroundGenerator bgGenerator = createBackgroundGenerator();
		FontGenerator fontGenerator = createFontGenerator();
		ImageDeformation[] bgDeformations = createBackgroundDeformations();
		ImageDeformation[] textDeformations = createTextDeformations();
		ImageDeformation[] finalDeformations = createFinalDeformations();
		WordToImage wordToImage = new DeformedComposedWordToImage(fontGenerator, bgGenerator, textPaster,
				bgDeformations, textDeformations, finalDeformations);

		return new SimpleEngine(new ImageCaptchaFactory[] {
				new SimpleFactory(wordToImage, wordGenerator, caseSensitive)
		});
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		checkArgument(width > 0, "width cannot be zero or negtive");
		checkArgument(height > 0, "height cannot be zero or negtive");
		checkArgument(length > 0, "length cannot be zero or negtive");
		checkArgument(maxFontSize > 0, "maxFontSize cannot be zero or negtive");

		if (fonts == null || fonts.length == 0) {
			if (fontNames != null) {
				List<Font> fontList = Lists.newArrayList();
				for (String fontName : fontNames) {
					fontList.add(new Font(fontName, Font.PLAIN, maxFontSize));
				}
				fonts = fontList.toArray(new Font[0]);
			} else {
				fonts = new Font[] {
						new Font(null, Font.PLAIN, maxFontSize)
				};
			}
		}

		if (Strings.isNullOrEmpty(challengeChars)) {
			challengeChars = DEFAULT_CHALLENGE_CHARS;
		}
	}

	@Override
	public ImageCaptchaEngine getObject() throws Exception {
		if (instance == null) {
			instance = createInstance();
		}
		return instance;
	}

	@Override
	public Class<?> getObjectType() {
		return ImageCaptchaEngine.class;
	}

	@Override
	public boolean isSingleton() {
		return true;
	}

	public void setWidth(int width) {
		this.width = width;
	}

	public void setHeight(int height) {
		this.height = height;
	}

	public void setLength(int length) {
		this.length = length;
	}

	public void setMaxFontSize(int maxFontSize) {
		this.maxFontSize = maxFontSize;
	}

	public void setFonts(Font[] fonts) {
		this.fonts = fonts;
	}

	public void setFontNames(String[] fontNames) {
		this.fontNames = fontNames;
	}

	public void setCaseSensitive(boolean caseSensitive) {
		this.caseSensitive = caseSensitive;
	}

	public void setChallengeChars(String challengeChars) {
		this.challengeChars = challengeChars;
	}

	private static final class SimpleEngine extends DefaultImageCaptchaEngine {
		public SimpleEngine(ImageCaptchaFactory[] factories) {
			super(factories);
		}
	}

	private static final class SimpleFactory extends ImageCaptchaFactory {
		private final Random random = new SecureRandom();
		private final WordToImage wordToImage;
		private final WordGenerator wordGenerator;
		private final boolean caseSensitive;

		public SimpleFactory(WordToImage wordToImage, WordGenerator wordGenerator, boolean caseSensitive) {
			this.wordToImage = wordToImage;
			this.wordGenerator = wordGenerator;
			this.caseSensitive = caseSensitive;
		}

		@Override
		public ImageCaptcha getImageCaptcha(Locale locale) {
			int max = wordToImage.getMaxAcceptedWordLength();
			int min = wordToImage.getMinAcceptedWordLength();
			int length = max == min ? min : min + random.nextInt(max - min);
			String word = wordGenerator.getWord(length, locale);

			BufferedImage image;
			try {
				image = wordToImage.getImage(word);
			} catch (Throwable e) {
				throw new CaptchaException(e);
			}
			return new SimpleCaptcha(null, image, word, caseSensitive);
		}

		@Override
		public ImageCaptcha getImageCaptcha() {
			return getImageCaptcha(Locale.getDefault());
		}
	}

	private static class SimpleCaptcha extends ImageCaptcha implements Serializable {
		private static final long serialVersionUID = 1367639617305794732L;

		private final String responseWord;
		private final boolean caseSensitive;

		public SimpleCaptcha(String question, BufferedImage challenge, String responseWord, boolean caseSensitive) {
			super(question, challenge);

			this.responseWord = responseWord;
			this.caseSensitive = caseSensitive;
		}

		@Override
		public Boolean validateResponse(Object response) {
			if (!(response instanceof String)) {
				return false;
			}
			String textResponse = (String) response;
			if (caseSensitive) {
				return textResponse.equals(responseWord);
			} else {
				return textResponse.equalsIgnoreCase(responseWord);
			}
		}
	}
}
